# ------------------------------------
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
import os
import json
from typing import Any, Optional, Dict

from azure.core.exceptions import ClientAuthenticationError, HttpResponseError
from azure.core.pipeline.transport import HttpRequest
from azure.core.credentials import AccessTokenInfo

from .. import CredentialUnavailableError
from .._constants import EnvironmentVariables
from .._internal import within_credential_chain
from .._internal.managed_identity_client import ManagedIdentityClient
from .._internal.msal_managed_identity_client import MsalManagedIdentityClient


IMDS_AUTHORITY = "http://169.254.169.254"
IMDS_TOKEN_PATH = "/metadata/identity/oauth2/token"

PIPELINE_SETTINGS = {
    "connection_timeout": 2,
    # Five retries, with each retry sleeping for [0.0s, 1.6s, 3.2s, 6.4s, 12.8s] between attempts.
    "retry_backoff_factor": 0.8,
    "retry_backoff_max": 60,
    "retry_on_status_codes": [404, 410, 429] + list(range(500, 600)),
    "retry_status": 5,
    "retry_total": 5,
}


def _get_request(scope: str, identity_config: Dict) -> HttpRequest:
    url = (
        os.environ.get(EnvironmentVariables.AZURE_POD_IDENTITY_AUTHORITY_HOST, IMDS_AUTHORITY).strip("/")
        + IMDS_TOKEN_PATH
    )
    request = HttpRequest("GET", url)
    request.format_parameters(dict({"api-version": "2018-02-01", "resource": scope}, **identity_config))
    return request


def _check_forbidden_response(ex: HttpResponseError) -> None:
    """Special case handling for Docker Desktop.

    Docker Desktop proxies all HTTP traffic, and if the IMDS endpoint is unreachable, it
    responds with a 403 with a message that contains "unreachable".

    :param ~azure.core.exceptions.HttpResponseError ex: The exception raised by the request
    :raises ~azure.core.exceptions.CredentialUnavailableError: When the IMDS endpoint is unreachable
    """
    if ex.status_code == 403:
        if ex.message and "unreachable" in ex.message:
            error_message = f"ManagedIdentityCredential authentication unavailable. Error: {ex.message}"
            raise CredentialUnavailableError(message=error_message) from ex


class ImdsCredential(MsalManagedIdentityClient):
    def __init__(self, **kwargs: Any) -> None:
        super(ImdsCredential, self).__init__(**kwargs)
        self._config = kwargs

        if EnvironmentVariables.AZURE_POD_IDENTITY_AUTHORITY_HOST in os.environ:
            self._endpoint_available: Optional[bool] = True
        else:
            self._endpoint_available = None

    def __enter__(self) -> "ImdsCredential":
        self._client.__enter__()
        return self

    def __exit__(self, *args):
        self._client.__exit__(*args)

    def close(self) -> None:
        self.__exit__()

    def _request_token(self, *scopes: str, **kwargs: Any) -> AccessTokenInfo:

        if within_credential_chain.get() and not self._endpoint_available:
            # If within a chain (e.g. DefaultAzureCredential), we do a quick check to see if the IMDS endpoint
            # is available to avoid hanging for a long time if the endpoint isn't available.
            try:
                client = ManagedIdentityClient(_get_request, **dict(PIPELINE_SETTINGS, **self._config))
                client.request_token(*scopes, connection_timeout=1, retry_total=0)
                self._endpoint_available = True
            except HttpResponseError as ex:
                # IMDS responded
                _check_forbidden_response(ex)
                self._endpoint_available = True
            except Exception as ex:  # pylint:disable=broad-except
                error_message = (
                    "ManagedIdentityCredential authentication unavailable, no response from the IMDS endpoint."
                )
                raise CredentialUnavailableError(error_message) from ex

        try:
            token_info = super()._request_token(*scopes)
        except CredentialUnavailableError:
            # Response is not json, skip the IMDS credential
            raise
        except HttpResponseError as ex:
            # 400 in response to a token request indicates managed identity is disabled,
            # or the identity with the specified client_id is not available
            if ex.status_code == 400:
                error_message = (
                    "ManagedIdentityCredential authentication unavailable. "
                    "No identity has been assigned to this resource."
                )

                if ex.message:
                    error_message += f" Error: {ex.message}"

                raise CredentialUnavailableError(message=error_message) from ex

            _check_forbidden_response(ex)
            # any other error is unexpected
            raise ClientAuthenticationError(message=ex.message, response=ex.response) from ex
        except json.decoder.JSONDecodeError as ex:
            raise CredentialUnavailableError(message="ManagedIdentityCredential authentication unavailable.") from ex
        except Exception as ex:  # pylint:disable=broad-except
            # if anything else was raised, assume the endpoint is unavailable
            error_message = "ManagedIdentityCredential authentication unavailable, no response from the IMDS endpoint."
            raise CredentialUnavailableError(error_message) from ex
        return token_info

    def get_unavailable_message(self, desc: str = "") -> str:
        return f"ManagedIdentityCredential authentication unavailable, no response from the IMDS endpoint. {desc}"
